CDKでFargateをデプロイする(実装・デプロイ編)
さて!今回で最終回となります。前回の記事はコチラ。
これまでReactとSpring BootでCognito認証・認可付きのWebアプリケーションを作ってきました。
それらのアプリケーションを1つのALBから振り分けるためのIaCをCDKを使ってデプロイしていきたいと思います。
ちょっとしたコンストラクトも作成します。
プロジェクトの作成
まずはCDKプロジェクトを作成していきます。
これまで読んできた方にはおなじみのコマンドです。
1 2 3 4 |
mkdir -p miso-cdk\miso-application cd miso-cdk\miso-application cdk init --language typescript npm install dotenv |
プロジェクトの作成が終わったら、環境変数を保存する.env系のファイルをGitに含めないようにしましょう。
1 2 3 4 5 6 7 8 |
!jest.config.js *.d.ts node_modules # CDK asset staging directory .cdk.staging cdk.out .env.* |
次に.envから設定を読み込みようにbinの下のmiso-application.tsを編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#!/usr/bin/env node import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import { MisoApplicationStack } from '../lib/miso-application-stack'; import * as dotenv from 'dotenv'; dotenv.config({ path: `.env.${process.env.NODE_ENV}` }); const app = new cdk.App(); new MisoApplicationStack(app, 'MisoApplicationStack', { }); |
miso-applicationの下にlambdaというフォルダを作成し、第5回で使用したJWT検証URLアクセス用のLambaのコードlambda_function.pyを配置しておきます。
まずはプロジェクトの準備が整いました。
ここまで作ってきたプロジェクトのフォルダ構成はこのようになっているはずです。
コンストラクトの作成
コードの保守性を高めるため、2つほどコンストラクト化します。
1つはVPC部分、もう1つはFargate部分です。
libの下にconstructsフォルダを作成し、そこにコンストラクトのソースを作っていきます。
まずはVPC部分のコンストラクトです。
他にもパラメータ化できますが、そこはお好きなように。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; interface FargatePrivateVpcProps { vpcName: string; vpcCidr: string; privateSubnetCidrMask: number; privateSubnetName: string; } export class FargatePrivateVpc extends Construct { public readonly vpc: ec2.Vpc; constructor(scope: Construct, id: string, props: FargatePrivateVpcProps) { super(scope, id); // VPCを作成 this.vpc = new ec2.Vpc(this, props.vpcName, { vpcName: props.vpcName, ipAddresses: ec2.IpAddresses.cidr(props.vpcCidr), maxAzs: 2, subnetConfiguration: [ { cidrMask: props.privateSubnetCidrMask, name: props.privateSubnetName, subnetType: ec2.SubnetType.PRIVATE_ISOLATED, }, ], natGateways: 0, }); // S3 VPCエンドポイントを作成 this.vpc.addGatewayEndpoint('S3Endpoint', { service: ec2.GatewayVpcEndpointAwsService.S3, }); // ECR API VPCエンドポイントを作成 this.vpc.addInterfaceEndpoint('EcrApiEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.ECR, }); // ECR Docker VPCエンドポイントを作成 this.vpc.addInterfaceEndpoint('EcrDockerEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER, }); // CloudWatch VPCエンドポイントを作成 this.vpc.addInterfaceEndpoint('CloudWatchEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS, }); } } |
プライベートサブネットだけのVPCとFargateの実行に必要なVPCエンドポイントを作成しています。
次にFargateにサービスを作成するコンストラクトを作ります。
今回フロントエンドとバックエンドのサービスを作るのですが、それをこのコンストラクトで共通的に作れるようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as ecr from 'aws-cdk-lib/aws-ecr'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as imagedeploy from 'cdk-docker-image-deployment'; interface BaseFargateApplicationProps { cluster: ecs.ICluster; repositoryName: string; dockerImagePath: string; logGroupName: string; taskRoleName: string; executionRoleName: string; memoryLimitMiB: number; cpu: number; containerEnvironment: { [key: string]: string }; containerPort: number; serviceName: string; subnetGroupName: string; desiredCount: number; buildArgs?: { [key: string]: string }; } export class BaseFargateApplication extends Construct { public readonly repository: ecr.IRepository; public readonly service: ecs.FargateService; constructor(scope: Construct, id: string, props: BaseFargateApplicationProps) { super(scope, id); this.repository = this.createEcrRepository(props.repositoryName, props.dockerImagePath, props.buildArgs); this.service = this.createFargateService( props.cluster, this.repository, props.logGroupName, props.taskRoleName, props.executionRoleName, props.memoryLimitMiB, props.cpu, props.containerEnvironment, props.containerPort, props.serviceName, props.subnetGroupName, props.desiredCount ); } private createEcrRepository(repositoryName: string, dockerImagePath: string, buildArgs?: { [key: string]: string }): ecr.IRepository { // ECRリポジトリの作成 const repository = new ecr.Repository(this, 'EcrRepository', { repositoryName: repositoryName, removalPolicy: cdk.RemovalPolicy.DESTROY, emptyOnDelete: true, lifecycleRules: [ { maxImageCount: 3 } ], encryption: ecr.RepositoryEncryption.KMS, imageScanOnPush: false, }); // DockerイメージをビルドしてECRにプッシュ new imagedeploy.DockerImageDeployment(this, 'ImageDeployment', { source: imagedeploy.Source.directory(dockerImagePath, { buildArgs: buildArgs, }), destination: imagedeploy.Destination.ecr(repository, { tag: 'latest', }), }); return repository; } private createFargateService( cluster: ecs.ICluster, repository: ecr.IRepository, logGroupName: string, taskRoleName: string, executionRoleName: string, memoryLimitMiB: number, cpu: number, containerEnvironment: { [key: string]: string }, containerPort: number, serviceName: string, subnetGroupName: string, desiredCount: number ): ecs.FargateService { // タスクロールの作成 const taskRole = new iam.Role(this, 'TaskRole', { roleName: taskRoleName, assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }); // CloudWatch Logsロググループの作成 const logGroup = new logs.LogGroup(this, 'LogGroup', { logGroupName: logGroupName, retention: logs.RetentionDays.THREE_MONTHS, removalPolicy: cdk.RemovalPolicy.DESTROY, }); // 実行ロールの作成 const executionRole = new iam.Role(this, 'ExecutionRole', { roleName: executionRoleName, assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }); executionRole.addToPolicy(new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'ecr:GetDownloadUrlForLayer', 'ecr:BatchGetImage', 'ecr:BatchCheckLayerAvailability', 'ecr:GetAuthorizationToken', ], resources: [repository.repositoryArn] })); executionRole.addToPolicy(new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'logs:CreateLogStream', 'logs:PutLogEvents', ], resources: [logGroup.logGroupArn] })); // タスク定義の作成 const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', { memoryLimitMiB: memoryLimitMiB, cpu: cpu, runtimePlatform: { cpuArchitecture: ecs.CpuArchitecture.X86_64, }, taskRole: taskRole, executionRole: executionRole, }); // コンテナの定義 const container = taskDefinition.addContainer('Container', { image: ecs.ContainerImage.fromEcrRepository(repository, 'latest'), environment: containerEnvironment, logging: new ecs.AwsLogDriver({ logGroup: logGroup, streamPrefix: 'service', }), }); container.addPortMappings({ containerPort: containerPort, }); // ECSサービスの作成 const service = new ecs.FargateService(this, 'Service', { cluster, serviceName: serviceName, taskDefinition: taskDefinition, desiredCount: desiredCount, vpcSubnets: { subnetGroupName: subnetGroupName, }, deploymentController: { type: ecs.DeploymentControllerType.ECS, }, }); return service; } } |
コードの内容を簡単に説明します。
ECRリポジトリ作成(createEcrRepository)
- ECR リポジトリを作成。削除時にはリポジトリも削除され、暗号化(KMS)される設定がされています。
- cdk-docker-image-deploymentを使って、指定されたディレクトリからDockerイメージをビルドし、ECRにプッシュします(タグは “latest”)。
Fargateサービス作成(createFargateService)
- IAMロールの作成
- タスクが実行するためのタスクロールと、ECRやCloudWatch Logsにアクセスするための実行ロールを作成しています。実行ロールには必要なポリシー(ECRからのイメージ取得、ログの書き込み)が付与されています。
- ロググループの作成
- CloudWatch Logsのロググループを作成し、ログの保持期間や削除ポリシーを設定しています。
- タスク定義の作成
- Fargate用のタスク定義を作成し、タスクに必要なリソース(メモリ、CPU、ロール)を設定します。また、コンテナ定義を追加し、ECRからビルドしたイメージを使用、環境変数、ログ ドライバ(AwsLogDriver)とポートマッピングを設定しています。
- ECS Fargateサービスの作成
- 作成したタスク定義およびその他のパラメータ(クラスター、サービス名、デザイアカウント数、対象サブネットなど)を利用して、Fargateサービスを作成します。ここでは、デプロイメントコントローラーのタイプとして ECSを指定しています。
スタックの作成
メインとなるロジックを作成します。
miso-application-stack.tsを編集して次のようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as path from 'path'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as targets from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import { BaseFargateApplication } from './constructs/base-fargate-application'; import { FargatePrivateVpc } from './constructs/fargate-private-vpc'; export class MisoApplicationStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // VPCの作成 const vpc = new FargatePrivateVpc(this, 'MisoVpc', { vpcName: 'MisoVpc', vpcCidr: process.env.VPC_CIDR || '10.0.0.0/16', privateSubnetCidrMask: 24, privateSubnetName: 'PrivateSubnet', }).vpc; // ECSクラスターの生成 const cluster = this.createEcsCluster(vpc); // ロードバランサの作成 const lb = this.createApplicationLoadBalancer(vpc, 'miso-alb'); // フロントエンドサービスの作成 const frontendApplicaion = new BaseFargateApplication(this, 'FrontendFargateApplication', { cluster, repositoryName: 'miso-frontend', dockerImagePath: path.join(__dirname, "../../../", "miso-frontend"), logGroupName: '/ecs/miso-frontend-service', taskRoleName: 'MisoFrontendTaskRole', executionRoleName: 'MisoFrontendExecutionRole', memoryLimitMiB: 512, cpu: 256, containerEnvironment: {}, containerPort: 80, serviceName: 'MisoFrontendService', subnetGroupName: 'PrivateSubnet', desiredCount: 1, buildArgs: { VITE_USER_POOL_ID: process.env.USER_POOL_ID || '', VITE_APP_CLIENT_ID: process.env.APP_CLIENT_ID || '', VITE_COGNITO_DOMAIN: process.env.COGNITO_DOMAIN || '', }, }); // バックエンドサービスの作成 const backendApplication = new BaseFargateApplication(this, 'BackendFargateApplication', { cluster, repositoryName: 'miso-backend', dockerImagePath: path.join(__dirname, "../../../", "miso-backend"), logGroupName: '/ecs/miso-backend-service', taskRoleName: 'MisoBackendTaskRole', executionRoleName: 'MisoBackendExecutionRole', memoryLimitMiB: 1024, cpu: 512, containerEnvironment: { JWK_SET_URI: `https://${lb.loadBalancerDnsName}/${process.env.USER_POOL_ID}/.well-known/jwks.json`, ALLOWED_ORIGINS: `https://${lb.loadBalancerDnsName}/`, }, containerPort: 8080, serviceName: 'MisoBackendService', subnetGroupName: 'PrivateSubnet', desiredCount: 1, }); // Lambdaの作成 const lambdaForCognito = this.createLambdaForCognito(); // ロードバランサの設定 this.configApplicationLoadBalancer(lb, frontendApplicaion.service, backendApplication.service, lambdaForCognito); } /** * ECSクラスターを作成します。 * * @param vpc - クラスターが配置されるVPC * @returns 作成されたECSクラスター */ createEcsCluster(vpc: ec2.IVpc) { const cluster = new ecs.Cluster(this, 'EcsCluster', { clusterName: 'MisoCluster', vpc: vpc, containerInsights: true, }); return cluster; } /** * アプリケーションロードバランサを作成します。 * * @param vpc - ロードバランサが配置されるVPC * @param loadBalancerName - ロードバランサの名前 * @returns 作成されたアプリケーションロードバランサ */ createApplicationLoadBalancer(vpc: ec2.IVpc, loadBalancerName: string) { // セキュリティグループの作成 const securityGroup = new ec2.SecurityGroup(this, 'AlbSecurityGroup', { securityGroupName: 'sec-miso-alb', vpc, description: 'Allow https traffic', }); securityGroup.addIngressRule(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(443), 'Allow from internal'); // ロードバランサの作成 const lb = new elbv2.ApplicationLoadBalancer(this, 'ApplicationLoadBalancer', { vpc, loadBalancerName: loadBalancerName, vpcSubnets: { subnetGroupName: 'PrivateSubnet', }, internetFacing: false, securityGroup: securityGroup, }); return lb; } /** * Cognito用のLambda関数を作成します。 * * @returns 作成されたLambda関数 */ createLambdaForCognito() { return new lambda.Function(this, 'CognitoJwkLambda', { runtime: lambda.Runtime.PYTHON_3_12, handler: 'lambda_function.lambda_handler', code: lambda.Code.fromAsset(path.join(__dirname, '../lambda')), environment: { COGNITO_JWK_SET_URI: `https://cognito-idp.ap-northeast-1.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`, }, }); } /** * アプリケーションロードバランサを設定します。 * * @param lb - 設定するアプリケーションロードバランサ * @param frontendService - フロントエンドサービスのFargateサービス * @param backendService - バックエンドサービスのFargateサービス * @param lambdaForCognito - Cognito用のLambda関数 */ configApplicationLoadBalancer(lb: elbv2.ApplicationLoadBalancer, frontendService: ecs.FargateService, backendService: ecs.FargateService, lambdaForCognito: lambda.Function) { // リスナーの追加 const listener = lb.addListener('Listener', { port: 443, protocol: elbv2.ApplicationProtocol.HTTPS, open: false, certificates: [{ certificateArn: process.env.CERTIFICATE_ARN || '', }], }); // フロントエンド用ターゲットグループの追加 listener.addTargets('TargetGroupFrontend', { targetGroupName: 'tg-miso-frontend', port: 80, targets: [frontendService], healthCheck: { path: '/', }, }); // バックエンド用ターゲットグループの追加 listener.addTargets('TargetGroupBackend', { targetGroupName: 'tg-miso-backend', port: 8080, targets: [backendService], healthCheck: { path: '/api/healthcheck', }, priority: 1, conditions: [elbv2.ListenerCondition.pathPatterns(['/api/*'])], }); // Cognito用Lambdaターゲットグループの追加 listener.addTargets('TargetGroupLambda', { targetGroupName: 'tg-miso-cognito-jwk', targets: [new targets.LambdaTarget(lambdaForCognito)], healthCheck: { enabled: true, }, priority: 2, conditions: [elbv2.ListenerCondition.pathPatterns([`/${process.env.USER_POOL_ID}/.well-known/jwks.json`])], }); } } |
コードが長いので、ポイントを絞って説明します。
VPCの作成
- 作成しておいたコンストラクトを使用してVPCを作成しています。
- 環境変数から渡されたVPCのCIDRを使用しています。
ECSクラスターの生成
- 作成したVPCにECSクラスターを作成しています。
- Container Insightsもここで有効にしています。
ロードバランサの作成
- ロードバランサに割り当てるセキュリティグループとロードバランサを作成しています。
- プライベートサブネット名を指定し、Internet FacingをOFFにすることによって、プライベートなロードバランサとしています。
フロントエンドサービスの作成
- 作成しておいたコンストラクトを使用して、フロントエンドサービスを作成しています。
- ビルドのときの引数に必要な環境変数を渡しています。
バックエンドサービスの作成
- 同様に作成しておいたコンストラクトを使用して、バックエンドサービスを作成しています。
- こちらはコンテナ実行時の環境変数に値を渡し、ロードバランサのJWT検証URL取得のパスを指定しています。
Lambdaの作成
- JWT検証URL取得用Lambdaを作成しています。
ロードバランサの設定
- 環境変数から渡されたACM証明書のARNを使用して、リスナーを作成しています。
- 作成したリスナーにパスルーティングで複数のターゲットグループを設定しています。
最後に.env.devを作成します。設定値は記載しませんが、以下のような値を設定してください。
1 2 3 4 5 |
VPC_CIDR= USER_POOL_ID= APP_CLIENT_ID= COGNITO_DOMAIN= CERTIFICATE_ARN= |
コードがすべて揃いました!デプロイしましょう!
1 2 |
export NODE_ENV=dev cdk deploy |
デプロイが完了したら最後にやることがあります。
Cognitoのアプリケーションクライアントに設定されている「許可されているコールバックURL」および「許可されているサインアウトURL」にALBのアドレスを追加してください。
こう考えるとカスタムドメインでやったほうが圧倒的に楽ですね・・・
まとめ
無事に想定していたWebアプリケーションをデプロイ・実行することができました!
これまで、SIerが携わることの多いプライベートな社内向けシステムを想定して、ReactとSpring Bootで作成したアプリケーションをCDKを使用してCognitoを作成するところからFargateにデプロイできるところまで連載してきました。
CDKは一度書いてしまえば、アプリケーションの開発者にとってもメンテナンスできそうなくらいわかりやすいコードになっていると思います。
今後もAWS案件に関わることになったら、積極的に使用していきたい大好きなサービスです。
今回の記事がどなたかのお役に立てれば幸いです。
それでは、またどこかの記事で👋
シリーズ記事
- React×Spring Bootな構成をAWS Fargateで動かす(1) ~ Cognitoの構築
- React×Spring Bootな構成をAWS Fargateで動かす(2) ~認証付きフロントエンドの作成
- React×Spring Bootな構成をAWS Fargateで動かす(3) ~バックエンドの作成
- React×Spring Bootな構成をAWS Fargateで動かす(4) ~認可機能の追加
- React×Spring Bootな構成をAWS Fargateで動かす(5) ~構成上の問題の解消方法
- React×Spring Bootな構成をAWS Fargateで動かす(6) ~ Fargateのデプロイ(準備編)
- React×Spring Bootな構成をAWS Fargateで動かす(7) ~ Fargateのデプロイ(実装・デプロイ編)
執筆者プロフィール

- tdi デジタルイノベーション技術部
-
昔も今も新しいものが大好き!
インフラからアプリまで縦横無尽にトータルサポートや新技術の探求を行っています。
週末はときどきキャンプ場に出没します。